package com.atguigu.gmall.list.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.atguigu.gmall.list.repository.GoodsRepository;
import com.atguigu.gmall.list.service.SearchService;
import com.atguigu.gmall.model.list.*;
import com.atguigu.gmall.model.product.*;
import com.atguigu.gmall.product.client.ProductFeignClient;
import lombok.extern.slf4j.Slf4j;
import org.apache.lucene.search.join.ScoreMode;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.common.text.Text;
import org.elasticsearch.index.query.*;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.SearchHits;
import org.elasticsearch.search.aggregations.Aggregation;
import org.elasticsearch.search.aggregations.AggregationBuilders;
import org.elasticsearch.search.aggregations.bucket.nested.ParsedNested;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedLongTerms;
import org.elasticsearch.search.aggregations.bucket.terms.ParsedStringTerms;
import org.elasticsearch.search.aggregations.bucket.terms.Terms;
import org.elasticsearch.search.aggregations.bucket.terms.TermsAggregationBuilder;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightBuilder;
import org.elasticsearch.search.fetch.subphase.highlight.HighlightField;
import org.elasticsearch.search.sort.SortOrder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.io.IOException;
import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@Service
public class SearchServiceImpl implements SearchService {

    @Autowired
    private ProductFeignClient productFeignClient;

    @Autowired
    private GoodsRepository goodsRepository;// 有对应操作 es数据的 CRUD

    @Autowired
    private RedisTemplate redisTemplate;

    @Autowired
    private RestHighLevelClient restHighLevelClient; // 引入操作 es 的类 | 编写 dsl 语句

    // 上架：将商品对应的 skuId 保存到 es 中
    @Override
    public void upperGoods(Long skuId) {
        // 创建 Goods 对象
        Goods goods = new Goods();
        /*
            goods需要保存的数据：
                skuInfo的基本信息
                分类数据
                品牌数据
                平台属性集合
         */

        // 获取 SkuInfo
        SkuInfo skuInfo = productFeignClient.getSkuInfo(skuId);

        // 给 goods 每一项赋值
        if (null != skuId) {// 判断 skuInfo 不为空

            // 设置 goods 的基本信息
            goods.setId(skuInfo.getId());
            goods.setTitle(skuInfo.getSkuName());
            goods.setCreateTime(new Date());
            goods.setPrice(skuInfo.getPrice().doubleValue());
            goods.setDefaultImg(skuInfo.getSkuDefaultImg());

            // 获取品牌信息
            BaseTrademark trademark = productFeignClient.getTrademark(skuInfo.getTmId());
            if (null != trademark) {
                // 设置 goods 的品牌信息
                goods.setTmId(trademark.getId());
                goods.setTmName(trademark.getTmName());
                goods.setTmLogoUrl(trademark.getLogoUrl());
            }

            // 获取分类数据
            BaseCategoryView categoryView = productFeignClient.getCategoryView(skuInfo.getCategory3Id());
            if (null != categoryView) {
                // 设置 goods 的分类数据
                goods.setCategory1Id(categoryView.getCategory1Id());
                goods.setCategory1Name(categoryView.getCategory1Name());
                goods.setCategory2Id(categoryView.getCategory2Id());
                goods.setCategory2Name(categoryView.getCategory2Name());
                goods.setCategory3Id(categoryView.getCategory3Id());
                goods.setCategory3Name(categoryView.getCategory3Name());
            }
        }

        // 获取平台属性值集合。目的：为了给 Goods 中的 List<SearchAttr> attrs 赋值
        List<BaseAttrInfo> attrList = productFeignClient.getAttrList(skuId);

        // 判断当前集合不能为空
        if (!CollectionUtils.isEmpty(attrList)) {
            /*
            Goods中存储的是List<SearchAttr>集合，需要集合里的每一个 SearchAttr 对象赋值
                SearchAttr:
                    private Long attrId;
                    private String attrValue;
                    private String attrName;
                需要为这三个属性赋值，然后封装为集合。
             */
            List<SearchAttr> searchAttrList = attrList.stream().map((baseAttrInfo) -> {
                /*
                    map————接收 Lambda，将元素转换成其他形式或提取信息。
                        接收一个函数作为参数，该函数会被应用到每个元素上，并将其映射成一个新的元素。
                    这里使用了：提取信息
                 */

                // 创建一个 SearchAttr 对象
                SearchAttr searchAttr = new SearchAttr();

                // 设置平台属性Id
                searchAttr.setAttrId(baseAttrInfo.getId());

                // 设置平台属性名
                searchAttr.setAttrName(baseAttrInfo.getAttrName());

                // 获取平台属性集合对象
                List<BaseAttrValue> attrValueList = baseAttrInfo.getAttrValueList();

                /*
                为什么 get(0) ？
                    因为通过 productFeignClient.getAttrList(skuId) 获取的数据中，一个 sku 对应的 平台属性 和 对应的平台属性值 只有一个!!!
                 */
                // 从平台属性集合中获取属性值名称
                String valueName = attrValueList.get(0).getValueName();

                // searchAttr.setAttrValue(BaseAttrValue.valueName);
                // 设置平台属性值
                searchAttr.setAttrValue(valueName);

                // 返回 searchAttr
                return searchAttr;

            }).collect(Collectors.toList());// 将 stream 流换换为 List 返回

            // 设置 goods 的平台属性集合
            goods.setAttrs(searchAttrList);
        }
        // 保存 goods 到 es 中
        goodsRepository.save(goods);
    }

    // 下架：将商品对应的 skuId 在 es 中删除
    @Override
    public void lowerGoods(Long skuId) {
        // 调用 ElasticsearchRepository 中封装好的 CRUD。goodsRepository 继承 ElasticsearchRepository
        goodsRepository.deleteById(skuId);
    }

    @Override
    public void incrHotScore(Long skuId) {
        // 更新热度排名 使用 redis | zset 数据类型

        // 定义key
        String hotKey = "hotScore";

        // 保存数据(用户访问后统计的结果)
        Double hotScoreCount = redisTemplate.opsForZSet().incrementScore(hotKey, "skuId:" + skuId, 1);

        // 每被访问10次就更新es的数据
        if (hotScoreCount % 10 == 0) {
            // 更新 es ：将原有数据中的 "hotScore" 改为最新的

            // 通过 skuId 找到 商品
            Optional<Goods> optional = goodsRepository.findById(skuId);

            // 更新商品的 "hotScore"
            Goods goods = optional.get();
            goods.setHotScore(Math.round(hotScoreCount));

            // 保存新的数据到 es
            goodsRepository.save(goods);
        }
    }

    @Override
    public SearchResponseVo search(SearchParam searchParam) throws IOException {
        /*
         实现思路：
         1 获取到动态生成的 dsl 语句
         2 利用这个 dsl 语句进行查询
         3 将查询的结果集转换为我们需要的对象 SearchResponseVo 搜索结果集实体
         */

        // 根据用户检索的条件 构建 dsl 语句 (返回检索请求对象)
        SearchRequest searchRequest = buildQueryDsl(searchParam);

        // 执行 dsl 语句 (返回检索响应对象 即 结果集)
        SearchResponse searchResponse = restHighLevelClient.search(searchRequest, RequestOptions.DEFAULT);
        log.info(" searchResponse ===>>> " + searchResponse);

        // 将查询的结果集转换为搜索结果集实体：searchResponse ---> SearchResponseVo搜索结果集实体
        SearchResponseVo searchResponseVo = parseSearchResult(searchResponse);

        // 为 searchResponseVo 赋值(分页设置默认值)
        searchResponseVo.setPageSize(searchParam.getPageSize());
        searchResponseVo.setPageNo(searchParam.getPageNo());

        // 总页数 （通过dsl语句查询出来的数据有总记录数total，在结果集转换为搜索结果集实体的时候 已经对 总记录数 total 进行赋值）
        long totalPages = (searchResponseVo.getTotal() + searchParam.getPageSize() - 1) / searchParam.getPageSize();
        // 赋值总页数
        searchResponseVo.setTotalPages(totalPages);

        // 返回搜索结果集
        return searchResponseVo;
    }

    /**
     * 查询结果集转换查询结果实体功能
     *
     * @param searchResponse
     * @return
     */
    private SearchResponseVo parseSearchResult(SearchResponse searchResponse) {
        // 声明对象
        SearchResponseVo searchResponseVo = new SearchResponseVo();
        /*
        赋值：
            private List<SearchResponseTmVo> trademarkList;
            private List<SearchResponseAttrVo> attrsList = new ArrayList<>();
            private List<Goods> goodsList = new ArrayList<>();
            private Long total;//总记录数
         */

        SearchHits hits = searchResponse.getHits();// 获取单独的 hits 节点数据

        // 获取商品数据的集合
        SearchHit[] subHits = hits.getHits();

        // 声明一个存储商品对象的集合
        List<Goods> goodsList = new ArrayList<>();

        if (null != subHits && subHits.length > 0) {
            // 循环遍历从集合获取每一个数据
            for (SearchHit subHit : subHits) {

                // subHit.getSourceAsString() 相当于获取到 source 对应的实体类 Goods
                String sourceAsString = subHit.getSourceAsString();

                // 将字符串转换为实体类 设置转换的类型Goods.class
                Goods goods = JSONObject.parseObject(sourceAsString, Goods.class);

                // 获取高亮（ source 商品数据中有 title 属性 需要高亮）
                // 细节： 由于 source 中的 title 不是高亮，如果是通过全文检索的模式，需获取高亮字段！
                HighlightField highlightField = subHit.getHighlightFields().get("title");
                if (null != highlightField) {
                    Text title = highlightField.getFragments()[0];

                    // 设置高亮 title
                    goods.setTitle(title.toString());
                }
                // 将每个 goods 存入商品对象的集合
                goodsList.add(goods);
            }
        }
        // 赋值商品数据
        searchResponseVo.setGoodsList(goodsList);

        // 获取品牌数据、获取平台属性数据：从聚合中的数据获取 无重复数据 将整个聚合看作 map
        Map<String, Aggregation> aggregationMap = searchResponse.getAggregations().asMap();

        // 将 tmIdAgg聚合看作 map，key 是 tmIdAgg，value 是 tmIdAgg 里的数据。
        // 需要使用 ParsedLongTerms 接收从而来获取 getBuckets()；
        // Aggregation tmIdAgg = trademarkAggregationMap.get("tmIdAgg"); Aggregation 无法获取 getBuckets()；
        ParsedLongTerms tmIdAgg = (ParsedLongTerms) aggregationMap.get("tmIdAgg");

        List<SearchResponseTmVo> trademarkList = tmIdAgg.getBuckets().stream().map(bucket -> {
            // 声明一个品牌对象
            SearchResponseTmVo searchResponseTmVo = new SearchResponseTmVo();

            // 获取 品牌的id 并 赋值
            String tmId = bucket.getKeyAsString();
            searchResponseTmVo.setTmId(Long.parseLong(tmId));

            // 将 buckets 下的聚合看作 map
            Map<String, Aggregation> tmIdSubMap = bucket.getAggregations().getAsMap();

            // 获取 品牌的名称
            // 将 tmNameAgg聚合 看作 map，key 是 tmNameAgg，value 是 tmNameAgg 里的数据。
            // 需要使用 ParsedStringTerms  接收从而来获取 getBuckets() ；Aggregation 无法获取  getBuckets()；
            ParsedStringTerms tmNameAgg = (ParsedStringTerms) tmIdSubMap.get("tmNameAgg");
            // 返回结果集tmNameAgg中的数据集合buckets中key只有一个值 故 tmNameAgg.getBuckets().get(0)
            String tmName = tmNameAgg.getBuckets().get(0).getKeyAsString();
            // 赋值 品牌的名称
            searchResponseTmVo.setTmName(tmName);

            // 获取 品牌的logoUrl 并 赋值
            // 将 tmLogoUrlAgg聚合 看作 map，key 是 tmLogoUrlAgg，value 是 tmLogoUrlAgg 里的数据。
            ParsedStringTerms tmLogoUrlAgg = (ParsedStringTerms) tmIdSubMap.get("tmLogoUrlAgg");
            String tmLogoUrl = tmLogoUrlAgg.getBuckets().get(0).getKeyAsString();
            searchResponseTmVo.setTmLogoUrl(tmLogoUrl);

            // 返回品牌对象
            return searchResponseTmVo;
        }).collect(Collectors.toList());// 将 Stream 流转换为 list
        // 保存品牌数据
        searchResponseVo.setTrademarkList(trademarkList);

        // 获取总记录数
        long totalHits = hits.getTotalHits();
        // 赋值总记录数
        searchResponseVo.setTotal(totalHits);

        // 获取平台属性 (数据类型是 nested)
        // 将 attrsAgg聚合 看作 map，key 是 attrsAgg，value 是 attrsAgg 里的数据。
        // Aggregation attrsAgg = aggregationMap.get("attrsAgg");
        // 转型为 nested 使用 ParsedNested 接收
        ParsedNested attrsAgg = (ParsedNested) aggregationMap.get("attrsAgg");

        // 获取 attrIdAgg 的数据
        ParsedLongTerms attrIdAgg = attrsAgg.getAggregations().get("attrIdAgg");

        // 获取 attrIdAgg 下的 buckets
        List<? extends Terms.Bucket> buckets = attrIdAgg.getBuckets();

        if (!CollectionUtils.isEmpty(buckets)) {
            // 使用流式编程获取获取
            List<SearchResponseAttrVo> attrList = buckets.stream().map((bucket) -> {
                // 声明平台属性对象
                SearchResponseAttrVo searchResponseAttrVo = new SearchResponseAttrVo();

                // 获取 平台属性id 并 赋值
                Number attrId = bucket.getKeyAsNumber();
                searchResponseAttrVo.setAttrId(attrId.longValue());

                // 获取平台属性名称的聚合attrNameAgg
                // Aggregation 转换为 ParsedStringTerms 获取 buckets 的数据
                ParsedStringTerms attrNameAgg = bucket.getAggregations().get("attrNameAgg");
                // 获取 平台属性名称
                String attrName = attrNameAgg.getBuckets().get(0).getKeyAsString();
                // 赋值 平台属性名称
                searchResponseAttrVo.setAttrName(attrName);

                // 获取平台属性值的聚合attrValueAgg
                // Aggregation 转换为 ParsedStringTerms 获取 buckets 的数据
                ParsedStringTerms attrValueAgg = bucket.getAggregations().get("attrValueAgg");

                // 获取平台属性值集合
                List<? extends Terms.Bucket> valueBucketsList = attrValueAgg.getBuckets();

                // 获取集合中的每个数据 使用map流式编程 获取key对应的value
                //List<String> valueList = valueBucketsList.stream().map(Terms.Bucket::getKeyAsString).collect(Collectors.toList());
                List<String> valueList = valueBucketsList.stream().map((bukcet) -> {
                    String attrValue = bukcet.getKeyAsString();
                    return attrValue;
                }).collect(Collectors.toList());

                // 赋值 平台属性值集合
                searchResponseAttrVo.setAttrValueList(valueList);

                return searchResponseAttrVo;

            }).collect(Collectors.toList());
            // 赋值商品对应的平台属性
            searchResponseVo.setAttrsList(attrList);
        }
        log.info("searchResponseVo ===>>>" + searchResponseVo);
        return searchResponseVo;
    }


    /**
     * 根据用户输入的检索条件制作dsl语句
     *
     * @param searchParam
     * @return
     */
    private SearchRequest buildQueryDsl(SearchParam searchParam) {
        // 构建查询器 (最外层的{})
        SearchSourceBuilder searchSourceBuilder = new SearchSourceBuilder();

        // query --->>> bool

        // 构建 BoolQueryBuilder
        BoolQueryBuilder boolQueryBuilder = QueryBuilders.boolQuery();

        // bool --->>> filter

        // 构建分类过滤 判断一级分类Id(用户在点击的时候，只能点击一个值，所以此处使用term)
        if (!StringUtils.isEmpty(searchParam.getCategory1Id())) {
            // filter --->>> term
            TermQueryBuilder category1Id = QueryBuilders.termQuery("category1Id", searchParam.getCategory1Id());

            boolQueryBuilder.filter(category1Id);
        }
        // 构建分类过滤 判断二级分类Id
        if (!StringUtils.isEmpty(searchParam.getCategory2Id())) {
            // filter --->>> term
            TermQueryBuilder category2Id = QueryBuilders.termQuery("category2Id", searchParam.getCategory2Id());

            boolQueryBuilder.filter(category2Id);
        }
        // 构建分类过滤 判断三级分类Id
        if (!StringUtils.isEmpty(searchParam.getCategory3Id())) {
            // filter --->>> term
            TermQueryBuilder category3Id = QueryBuilders.termQuery("category3Id", searchParam.getCategory3Id());

            boolQueryBuilder.filter(category3Id);
        }

        // bool --->>> filter

        // filter --->>> term
        // 构建品牌查询
        // 获取品牌
        String trademark = searchParam.getTrademark();// trademark = 2:华为

        if (!StringUtils.isEmpty(trademark)) {
            // 按照冒号分隔品牌 分隔后数组长度为2
            String[] split = trademark.split(":");
            // 判断分隔后的数据格式是否正确
            if (null != split && split.length == 2) {
                // 根据品牌Id过滤
                // filter --->>> term "tmId":"4"
                boolQueryBuilder.filter(QueryBuilders.termQuery("tmId", split[0]));
            }
        }

        // bool --->>> filter

        // 构建平台属性查询
        // 获取平台属性
        String[] props = searchParam.getProps();// props=23:4G:运行内存

        if (null != props && props.length > 0) {
            // 循环遍历每一组数据
            for (String prop : props) {
                // prop = 23:4G:运行内存 对应 平台属性Id:平台属性值名称:平台属性名

                // 拆分数据 平台属性Id:平台属性值名称:平台属性名
                String[] split = prop.split(":");

                // 判断数据格式是否正确
                if (null != split && split.length == 3) {

                    // 构建 boolQuery
                    BoolQueryBuilder boolQuery = QueryBuilders.boolQuery();
                    // 构建 boolQuery 设置子查询条件
                    BoolQueryBuilder subBoolQuery = QueryBuilders.boolQuery();

                    // 设置子查询条件
                    // must --->>> term
                    subBoolQuery.must(QueryBuilders.termQuery("attrs.attrId", split[0]));// 平台属性Id
                    subBoolQuery.must(QueryBuilders.termQuery("attrs.attrValue", split[1])); // 平台属性值名称

                    // 设置 nested 查询
                    // bool --->>> must --->>> nested
                    boolQuery.must(QueryBuilders.nestedQuery("attrs", subBoolQuery, ScoreMode.None));

                    // filter --->>> bool --->>> must --->>> nested
                    boolQueryBuilder.filter(boolQuery);
                }
            }
        }

        // bool --->>> must

        // 判断查询条件是否为空 关键字
        if (!StringUtils.isEmpty(searchParam.getKeyword())) {
            // must --->>> match
            // MatchQueryBuilder matchQueryBuilder = new MatchQueryBuilder("title",searchParam.getKeyword());
            // operator(Operator.AND)：表示查询要同时满足关键字经过分词后所组成的不同的词
            // 比如：小米手机；分词后：小米 | 手机；查询的时候：小米And手机同时满足
            MatchQueryBuilder title = QueryBuilders.matchQuery("title", searchParam.getKeyword()).operator(Operator.AND);

            boolQueryBuilder.must(title);
        }

        // 构建排序
        // 默认按照热度排序。电商页面也可以选择排序：按销量、价格等排序。
        // 排序规则 1:hotScore | 2:price
        // 如果页面点击的是 综合=1 | 价格=2 ， &order=1:asc
        // &order=1:asc 表示热度升序排序  &order=1:desc 表示按热度降序排列
        // &order=2:asc 表示价格升序排序  &order=2:desc 表示价格度降序排列

        // 获取排序规则 1:asc...
        String order = searchParam.getOrder();

        if (!StringUtils.isEmpty(order)) {
            // 拆分数据 1:asc
            String[] split = order.split(":");

            // 判断数据格式
            if (null != split && split.length == 2) {
                // 声明一个排序的字段
                String field = null;

                // 判断排序字段 排序规则 1:hotScore | 2:price
                switch (split[0]) {
                    case "1":
                        field = "hotScore";
                        break;
                    case "2":
                        field = "price";
                        break;
                }
                // 设置 es 排序规则
                searchSourceBuilder.sort(field, "asc".equals(split[1]) ? SortOrder.ASC : SortOrder.DESC);

            } else { // 如果数据格式不正确 给默认排序规则
                searchSourceBuilder.sort("hotScore", SortOrder.DESC);
            }
        }

        // 构建分页
        // 设置起始页 (pageNo-1)*PageSize
        int from = (searchParam.getPageNo() - 1) * searchParam.getPageSize();
        searchSourceBuilder.from(from);
        searchSourceBuilder.size(searchParam.getPageSize());

        // 构建高亮
        // 创建高亮对象
        HighlightBuilder highlightBuilder = new HighlightBuilder();
        highlightBuilder.field("title");// 设置高亮字段
        highlightBuilder.preTags("<span style=color:red>");// 设置前缀
        highlightBuilder.postTags("</span>");// 设置后缀

        searchSourceBuilder.highlighter(highlightBuilder);

        // 设置品牌聚合
        TermsAggregationBuilder tmIdTermsAggregationBuilder =
                AggregationBuilders.terms("tmIdAgg").field("tmId")
                        .subAggregation(AggregationBuilders.terms("tmNameAgg").field("tmName"))
                        .subAggregation(AggregationBuilders.terms("tmLogoUrlAgg").field("tmLogoUrl"));

        searchSourceBuilder.aggregation(tmIdTermsAggregationBuilder);

        // 设置平台属性聚合 数据类型是 nested
        // nested 两个参数 第一个参数是一个变量名称 第二个参数是指定哪个字段是 nested 数据类型
        searchSourceBuilder.aggregation(AggregationBuilders.nested("attrsAgg", "attrs")
                .subAggregation(AggregationBuilders.terms("attrIdAgg").field("attrs.attrId")
                        .subAggregation(AggregationBuilders.terms("attrNameAgg").field("attrs.attrName"))
                        .subAggregation(AggregationBuilders.terms("attrValueAgg").field("attrs.attrValue"))));

        // query --->>> bool
        // 调用 query 方法 参数 QueryBuilder
        searchSourceBuilder.query(boolQueryBuilder);

        // 结果集的过滤：查询的结果集中只要 "id","defaultImg","title","price" 四个字段
        searchSourceBuilder.fetchSource(new String[]{"id", "defaultImg", "title", "price"}, null);

        // 创建查询的请求
        // 指定在哪个索引库、哪个类型查询 GET /goods/info/_search
        SearchRequest searchRequest = new SearchRequest("goods");
        searchRequest.types("info");
        // 将整个查询器放入 source 中
        searchRequest.source(searchSourceBuilder);
        log.info("dsl语句 ===>>> " + searchSourceBuilder);

        // 返回检索请求对象
        return searchRequest;
    }
}
